// Copyright 2022 Jeroen van Nugteren

// Permission is hereby granted, free of charge, to any person obtaining a copy of this software
// and associated documentation files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:

// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.

// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
// OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

// include header
#include "dfarray.hh"

// common headers
#include "rat/common/extra.hh"

// distmesh headers
#include "dftransform.hh"
#include "dfunion.hh"

// code specific to Rat
namespace rat{namespace dm{

	// constructor
	DFArray::DFArray(){
		set_name("Array");
	}

	// constructor
	DFArray::DFArray(
		const ShDistFunPr& df, 
		const arma::uword num_x, 
		const arma::uword num_y, 
		const fltp dx, const fltp dy, 
		const bool is_centered) : DFArray(){
		add_df(df); set_num_x(num_x); set_num_y(num_y); 
		set_dx(dx); set_dy(dy); set_is_centered(is_centered);
	}

	// factory
	ShDFArrayPr DFArray::create(){
		return std::make_shared<DFArray>();
	}

	// factory
	ShDFArrayPr DFArray::create(
		const ShDistFunPr& df, 
		const arma::uword num_x, 
		const arma::uword num_y, 
		const fltp dx, const fltp dy, 
		const bool is_centered){
		return std::make_shared<DFArray>(df,num_x,num_y,dx,dy,is_centered);
	}

	// add parts
	void DFArray::add(const ShDistFunPr& df){
		add_df(df);
	}

	// add distance function
	arma::uword DFArray::add_df(const ShDistFunPr &df){
		const arma::uword index = df_.size()+1;
		df_.insert({index,df}); return index;
	}
	
	// retreive distance function at index
	const ShDistFunPr& DFArray::get_df(const arma::uword index) const{
		auto it = df_.find(index);
		if(it==df_.end())rat_throw_line("index does not exist");
		return (*it).second;
	}
	
	// delete distance function at index
	bool DFArray::delete_df(const arma::uword index){	
		auto it = df_.find(index);
		if(it==df_.end())return false;
		(*it).second = NULL; return true;
	}

	// number of distance functions
	arma::uword DFArray::num_df() const{
		return df_.size();
	}

	// re-index nodes after deleting
	void DFArray::reindex(){
		std::map<arma::uword, ShDistFunPr> new_df; arma::uword idx = 1;
		for(auto it=df_.begin();it!=df_.end();it++)
			if((*it).second!=NULL)new_df.insert({idx++, (*it).second});
		df_ = new_df;
	}

	// setters
	void DFArray::set_is_centered(const bool is_centered){
		is_centered_ = is_centered;
	}

	void DFArray::set_num_x(const arma::uword num_x){
		num_x_ = num_x;
	}

	void DFArray::set_num_y(const arma::uword num_y){
		num_y_ = num_y;
	}
	
	void DFArray::set_dx(const fltp dx){
		dx_ = dx;
	}

	void DFArray::set_dy(const fltp dy){
		dy_ = dy;
	}

	// getters
	bool DFArray::get_is_centered()const{
		return is_centered_;
	}
	
	arma::uword DFArray::get_num_x()const{
		return num_x_;
	}
	
	arma::uword DFArray::get_num_y()const{
		return num_y_;
	}
	
	fltp DFArray::get_dx()const{
		return dx_;
	}

	fltp DFArray::get_dy()const{
		return dy_;
	}

	// create union
	ShDistFunPr DFArray::create_array()const{
		// check validity
		is_valid(true);

		// create union distance function
		const ShDFUnionPr dfuni = DFUnion::create();

		// walk over copies in the array
		for(arma::uword i=0;i<num_x_;i++)
			for(arma::uword j=0;j<num_y_;j++)
				dfuni->add_df(DFTransform::create(DFUnion::create(df_),{
					i*dx_ - (is_centered_ ? (num_x_-1)*dx_/2 : 0.0),
					j*dy_ - (is_centered_ ? (num_y_-1)*dy_/2 : 0.0)},RAT_CONST(0.0)));

		// return distance function
		return dfuni;
	}


	// perimeter function
	ShPerimeterPr DFArray::create_perimeter(const fltp delem) const{
		return create_array()->create_perimeter(delem);
	}

	// get bounding box
	arma::Mat<fltp>::fixed<2,2> DFArray::get_bounding() const{
		return create_array()->get_bounding();
	}

	// get fixed points
	arma::Mat<fltp> DFArray::get_fixed(const fltp abstol) const{
		return create_array()->get_fixed(abstol);
	}

	// distance function
	arma::Col<fltp> DFArray::calc_distance(const arma::Mat<fltp> &p) const{
		return create_array()->calc_distance(p);
	}

	// validity check
	bool DFArray::is_valid(const bool enable_throws)const{
		if(df_.empty()){if(enable_throws){rat_throw_line("requires at least one input distance function");} return false;};
		for(auto it=df_.begin();it!=df_.end();it++)if(!(*it).second->is_valid(enable_throws))return false;
		if(num_x_==0){if(enable_throws){rat_throw_line("number of copies in x must be at least one");} return false;};
		if(num_y_==0){if(enable_throws){rat_throw_line("number of copies in y must be at least one");} return false;};
		return true;
	}

	// type string for serialization
	std::string DFArray::get_type(){
		return "rat::dm::dfarray";
	}

	// method for serialization into json
	void DFArray::serialize(Json::Value &js, cmn::SList &list) const{
		// serialize parent
		DistFun::serialize(js,list);

		// store type
		js["type"] = get_type();

		// properties
		js["is_centered"] = is_centered_;
		js["num_x"] = static_cast<int>(num_x_);
		js["num_y"] = static_cast<int>(num_y_);
		js["dx"] = dx_;
		js["dy"] = dy_;

		// distance functions
		for(auto it=df_.begin();it!=df_.end();it++)
		 	js["distfuns"].append(cmn::Node::serialize_node((*it).second,list));
	}

	// method for deserialisation from json
	void DFArray::deserialize(
		const Json::Value &js, cmn::DSList &list, 
		const cmn::NodeFactoryMap &factory_list, 
		const boost::filesystem::path &pth){
		
		// deserialize parent
		DistFun::deserialize(js,list,factory_list,pth);

		// parent
		set_is_centered(js["is_centered"].asBool());
		set_num_x(js["num_x"].asUInt64());
		set_num_y(js["num_y"].asUInt64());
		set_dx(js["dx"].ASFLTP());
		set_dy(js["dy"].ASFLTP());

		// create distance functions
		for(auto it = js["distfuns"].begin();it!=js["distfuns"].end();it++){
			const ShDistFunPr df = cmn::Node::deserialize_node<DistFun>(*it, list, factory_list, pth);
			if(df==NULL)rat_throw_line("could not deserialize distance function");
			add_df(df);
		}
	}

}}